Suite de Text Zoom — Dev
Actualizado: 10/7/25
Ticket: https://github.com/a11ysolutions/a11y-issue-detector/issues/407
Estructura de la suite
python
TEXT_ZOOM_TEST_SUITE = [
TestCase(
[
IsZoomeable(),
SetViewport(width=1366, height=768),
TakeScreenshot("before_zoom", full_page_on_missing_element=True),
GetAllTextElements(store_key="text_elements_before_zoom"),
CaptureAccessibilityTree(
store_key="ax_tree_before_zoom", save_to_file="ax_tree_before_zoom.json"
),
ApplyRealZoom(zoom_percent=200),
TakeScreenshot("after_zoom", full_page_on_missing_element=True),
GetAllTextElements(store_key="text_elements_after_zoom"),
CaptureAccessibilityTree(
store_key="ax_tree_after_zoom", save_to_file="ax_tree_after_zoom.json"
),
DetectMissingTextElements(
store_key="text_elements_before_zoom",
store_key_2="text_elements_after_zoom",
),
],
"Text zoom test using real 200% browser zoom (extension) at 1366x768 with content and functionality verification (WCAG 1.4.4) - Complete accessibility approach",
),
]Core
- Usa el enfoque del árbol de accesibilidad
- Emula el zoom del navegador mediante una miniextensión
- Firmas, deduplicación y sistema semántico
- Contexto persistente solo para los tests relacionados con zoom
Cómo funciona
1. Comprobar si la página admite zoom y fijar el viewport
As described at:
2. Obtener todos los elementos de texto
Filters all text nodes using _is_text_element(element) and then the node is mapped into element_data and captured in text_elements::
2.1 Estructura del nodo del árbol AX en bruto
json
{
"nodeId": "3969",
"backendDOMnodeId": 3969,
"role": {"type": "role", "value": "button"},
"name": {
"type": "computedString",
"value": "Seleccionar divisa",
"sources": [
{"type": "relatedElement", "attribute": "aria-labelledby"},
{
"type": "attribute",
"attribute": "aria-label",
"attributeValue": {"type": "string", "value": "Seleccionar divisa"}
},
{"type": "contents", "value": {"value": "$ USD"}, "superseded": true}
]
}
}2.2 Datos del nodo ya mapeados
python
element_data = {
"nodeId": node.get("nodeId"),
"backendNodeId": backend_id,
"backendDOMNodeId": node.get("backendDOMNodeId"),
"name": node.get("name", {}).get("value", ""),
"role": node.get("role", {}).get("value", ""),
"description": node.get("description", {}).get("value", ""),
"value": node.get("value", {}).get("value", ""),
"properties": node.get("properties", []),
"text_content": self._get_text_content_from_ax_data(node),
"name_sources": node.get("name", {}).get("sources", []),
}
text_elements.append(element_data)2.3 element_data en bruto
python
{
"nodeId": "3969",
"backendNodeId": 3969,
"backendDOMNodeId": 3969,
"name": "Seleccionar divisa",
"role": "button",
"name_sources": [
{
"type": "relatedElement",
"attribute": "aria-labelledby"
},
{
"type": "attribute",
"value": {
"type": "computedString",
"value": "Seleccionar divisa"
},
"attribute": "aria-label",
"attributeValue": {
"type": "string",
"value": "Seleccionar divisa"
}
},
{
"type": "relatedElement",
"superseded": True,
"nativeSource": "label"
},
{
"type": "contents",
"value": {
"type": "computedString",
"value": "$ USD"
},
"superseded": True
},
{
"type": "attribute",
"attribute": "title",
"superseded": True
}
],
"description": "",
"value": "",
"properties": [...],
"text_content": "Seleccionar divisa"
}3. Deduplicación antes del zoom
3.1 Primera pasada — agrupación por nombre
python
elements_by_name = {
"Seleccionar divisa": [element0],
"Search": [element1, element2], # 2 elements: button and StaticText
# ... other groups
}python
# Just 1 element, no duplicateds
if len(element_group) == 1: # True
first_pass_elements.extend(element_group) # Keep it equal
# first_pass_elements:
first_pass_elements = [
{
"nodeId": "3969",
"name": "Seleccionar divisa",
"role": "button",
"name_sources": [...],
"backendDOMNodeId": 3969
},
# ... other elements
]3.2 Segunda pasada — deduplicación por firma
python
# Create signature
signature = self._create_text_element_signature(element_data)
# Signature: "role:button|name:Seleccionar divisa|bid:3969"
if signature not in signature_to_element: # True (first time)
signature_to_element[signature] = element_data
final_elements.append(element_data) # ← Add it
# final_elements now contains the element:
final_elements = [
{
"nodeId": "3969",
"name": "Seleccionar divisa",
"role": "button",
"name_sources": [...],
"backendDOMNodeId": 3969
},
# ... another elements
]4. Aplicar el zoom del navegador
DOM/AX Tree re-organized, framework re-renders . . .
5. Captura después del zoom
json
{
"nodeId": "3875", // ← it changes
"backendDOMNodeId": 3875, // ← it changes
"role": {"type": "role", "value": "button"},
"name": {
"type": "computedString",
"value": "Seleccionar divisa", // ← same
"sources": [ // ← same content
{"type": "relatedElement", "attribute": "aria-labelledby"},
{
"type": "attribute",
"attribute": "aria-label",
"attributeValue": {"type": "string", "value": "Seleccionar divisa"}
},
{"type": "contents", "value": {"value": "$ USD"}, "superseded": true}
]
}
}5.1 Procesado del elemento (igual que antes)
python
element_data_after = {
"nodeId": "3875", # ← it changes
"backendNodeId": 3875, # ← it changes
"backendDOMNodeId": 3875, # ← it changes
"name": "Seleccionar divisa", # ← same
"role": "button", # ← same
"name_sources": [ # ← same content
{"type": "relatedElement", "attribute": "aria-labelledby"},
{"type": "attribute", "attribute": "aria-label",
"attributeValue": {"type": "string", "value": "Seleccionar divisa"}},
{"type": "contents", "value": {"value": "$ USD"}, "superseded": True}
],
"description": "",
"value": "",
"properties": [...],
"text_content": "Seleccionar divisa"
}5.2 Deduplicación (igual que antes)
python
# generated signature:
signature_after = "role:button|name:Seleccionar divisa|bid:3875" # ← Diferent ID
elements_after_deduplicated = [
element_data_after,
# ... another elements
]6. Comparación y detección
6.1 Conjuntos de firmas
python
elements_before_set = {
"role:button|name:Seleccionar divisa|bid:3969", # ← BEFORE
"role:textbox|name:Search|bid:6",
# ... another signatures
}
elements_after_set = {
"role:button|name:Seleccionar divisa|bid:3875", # ← AFTER (diferent ID)
"role:textbox|name:Search|bid:6",
# ... otras firmas
}
# Missing elements:
missing_signatures = elements_before_set - elements_after_set
# missing_signatures = {"role:button|name:Seleccionar divisa|bid:3969"}6.2 Iterar sobre los elementos ausentes
python
for elem in elements_before_deduplicated:
if self._create_text_element_signature(elem) in missing_signatures:
# elem structure:
elem = {
"nodeId": "3969",
"name": "Seleccionar divisa",
"role": "button",
"name_sources": [...], # ← TODAVÍA AQUÍ
"backendDOMNodeId": 3969
}
backend_id = elem.get("backendNodeId") or elem.get("backendDOMNodeId")
# backend_id = 39696.3 Existencia por backend ID
python
exists_after = any(
(e.get("backendNodeId") or e.get("backendDOMNodeId")) == 3969
for e in elements_after_deduplicated
)
# exists_after = False (because now it is 3875, not 3969)6.4 Coincidencia semántica
python
semantic_match = self._find_semantic_match(elem, elements_after_deduplicated)
# Inisde _find_semantic_match:
# elem = {
# "name": "Seleccionar divisa",
# "role": "button",
# "name_sources": [...],
# "backendDOMNodeId": 3969
# }
# Extract values
role_before = elem.get("role", "") # "button"
name_before = elem.get("name", "") # "Seleccionar divisa"
aria_before = self._extract_aria_label(elem) # "Seleccionar divisa"
# Iterar sobre elements_after_deduplicated
for element_after in elements_after_deduplicated:
# element_after = {
# "name": "Seleccionar divisa",
# "role": "button",
# "name_sources": [...],
# "backendDOMNodeId": 3875
# }
role_after = element_after.get("role", "") # "button"
name_after = element_after.get("name", "") # "Seleccionar divisa"
aria_after = self._extract_aria_label(element_after) # "Seleccionar divisa"
if (role_after == role_before # True
and name_after == name_before # True
and aria_after == aria_before): # True
return element_after # ✅ FINDED MATCH!
# semantic_match = {
# "nodeId": "3875",
# "name": "Seleccionar divisa",
# "role": "button",
# "name_sources": [...],
# "backendDOMNodeId": 3875 # ← NEW ID
# }6.5 Update Backend ID:
python
if semantic_match:
new_backend_id = semantic_match.get("backendNodeId") or semantic_match.get("backendDOMNodeId")
# new_backend_id = 3875
if new_backend_id:
backend_id = new_backend_id # backend_id = 3875 (updated)
exists_after = True # ✅ now it exists6.6 Verify visibility:
python
if exists_after: # True
logger.log("Element Seleccionar divisa still exists after zoom (backend_id: 3875), checking visibility")
# Resolving with New backend_id (3875)
dom_element = context.page.resolve_backend_node_to_element(3875)
if dom_element and dom_element.is_visible():
logger.log("Element Seleccionar divisa is visible after zoom - not reporting as lost")
continue # ✅ It is not reported